package com.googlecode.yatspec.parsing;

import com.googlecode.totallylazy.Callable1;
import com.googlecode.totallylazy.Option;
import com.googlecode.totallylazy.Sequence;
import com.googlecode.yatspec.state.ScenarioTable;
import com.googlecode.yatspec.state.TestMethod;
import com.thoughtworks.qdox.model.Annotation;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.googlecode.totallylazy.Files.*;
import static com.googlecode.totallylazy.Option.option;
import static com.googlecode.totallylazy.Predicates.where;
import static com.googlecode.totallylazy.Sequences.empty;
import static com.googlecode.totallylazy.Sequences.sequence;
import static com.googlecode.totallylazy.Strings.endsWith;
import static com.googlecode.totallylazy.URLs.toURL;
import static com.googlecode.yatspec.parsing.Files.toPath;

public class ScalaTestMethodCollector implements TestMethodCollector {
    @Override
    public Sequence<TestMethod> collectTestMethods(Class aClass, Sequence<Method> methods) {
        final Option<ScalaClass> scalaClass = getScalaClass(aClass);
        if (scalaClass.isEmpty()) {
            return empty();
        }

        Map<String, List<ScalaMethod>> sourceMethodsByName = getMethods(scalaClass.get()).toMap(sourceMethodName());
        Map<String, List<Method>> reflectionMethodsByName = methods.toMap(reflectionMethodName());

        List<TestMethod> testMethods = new ArrayList<TestMethod>();
        // TODO Handling table tests
        for (String name : sourceMethodsByName.keySet()) {
            List<ScalaMethod> scalaMethods = sourceMethodsByName.get(name);
            List<Method> reflectionMethods = reflectionMethodsByName.get(name);
            testMethods.add(new TestMethod(aClass, reflectionMethods.get(0), name, new ScalaSource(scalaMethods.get(0)), new ScenarioTable()));
        }

        Sequence<TestMethod> myTestMethods = sequence(testMethods);
        Sequence<TestMethod> parentTestMethods = collectTestMethods(aClass.getSuperclass(), methods);

        return myTestMethods.join(parentTestMethods);
    }

    private static Callable1<ScalaMethod, String> sourceMethodName() {
        return new Callable1<ScalaMethod, String>() {
            @Override
            public String call(ScalaMethod scalaMethod) throws Exception {
                return scalaMethod.getName();
            }
        };
    }


    private Sequence<ScalaMethod> getMethods(ScalaClass scalaClass) {
        // TODO ignoring Annotations
        //return sequence(scalaClass.getMethods()).filter(where(annotations(), AnnotationNameCallable1.contains(Test.class)));
        return sequence(scalaClass.getTestMethods());
    }

    private Option<ScalaClass> getScalaClass(Class aClass) {
        Option<URL> option = getScalaSourceFromClassPath(aClass);
        option = !option.isEmpty() ? option : getScalaSourceFromFileSystem(aClass);
        return option.map(asAScalaClass(aClass));
    }

    private Option<URL> getScalaSourceFromFileSystem(Class aClass) {
        return isDefaultSuperClass(aClass) ? NO_URL : option(aClass.getClassLoader().getResource(toScalaPath(aClass)));
    }

    private String toScalaPath(Class aClass) {
        return toPath(aClass) + ".scala";
    }

    private Option<URL> getScalaSourceFromClassPath(Class aClass) {
        return isDefaultSuperClass(aClass) ? NO_URL : recursiveFiles(workingDirectory()).find(where(path(), endsWith(toScalaPath(aClass)))).map(toURL());
    }

    private Callable1<? super URL, ScalaClass> asAScalaClass(final Class aClass) {
        return new Callable1<URL, ScalaClass>() {
            @Override
            public ScalaClass call(URL url) throws Exception {
                return new ScalaClass(url);
            }
        };
    }

    private static Callable1<? super Method, String> reflectionMethodName() {
        return new ReflectionMethodNameCallable1();
    }

    private boolean isDefaultSuperClass(Class aClass) {
        // TODO what about Any or AnyRef?
        return aClass.equals(Object.class);
    }

    public static Callable1<? super ScalaMethod, Sequence<Annotation>> annotations() {
        return new Callable1<ScalaMethod, Sequence<Annotation>>() {
            @Override
            public Sequence<Annotation> call(ScalaMethod scalaMethod) throws Exception {
                return sequence(scalaMethod.getAnnotations());
            }
        };
    }
}
